package com.android.clinic.app.photo;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.ImageView;

@SuppressLint("AppCompatCustomView")
public class ImageViewTouchBase extends ImageView {
	private static final float SCALE_RATE = 1.25F;

	// This is the base transformation which is used to show the image
	// initially. The current computation for this shows the image in
	// it's entirety, letterboxing as needed. One could choose to
	// show the image as cropped instead.
	//
	// This matrix is recomputed when we go from the thumbnail image to
	// the full size image.
	protected Matrix baseMatrix = new Matrix();

	// This is the supplementary transformation which reflects what
	// the user has done in terms of zooming and panning.
	//
	// This matrix remains the same when we go from the thumbnail image
	// to the full size image.
	protected Matrix suppMatrix = new Matrix();

	// This is the final matrix which is computed as the concatentation
	// of the base matrix and the supplementary matrix.
	private final Matrix displayMatrix = new Matrix();

	// Temporary buffer used for getting the values out of a matrix.
	private final float[] matrixValues = new float[9];

	// The current bitmap being displayed.
	protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0);

	int thisWidth = -1;
	int thisHeight = -1;

	float maxZoom;

	private Runnable onLayoutRunnable;

	protected Handler handler = new Handler();

	// ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
	// its use of that Bitmap
	public interface Recycler {
		public void recycle(Bitmap b);
	}

	private Recycler recycler;

	public ImageViewTouchBase(Context context) {
		super(context);
		init();
	}

	public ImageViewTouchBase(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	public void setRecycler(Recycler recycler) {
		this.recycler = recycler;
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		thisWidth = right - left;
		thisHeight = bottom - top;
		Runnable r = onLayoutRunnable;
		if (r != null) {
			onLayoutRunnable = null;
			r.run();
		}
		if (bitmapDisplayed.getBitmap() != null) {
			getProperBaseMatrix(bitmapDisplayed, baseMatrix, true);
			setImageMatrix(getImageViewMatrix());
		}
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
			event.startTracking();
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}

	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
			if (getScale() > 1.0f) {
				// If we're zoomed in, pressing Back jumps out to show the
				// entire image, otherwise Back returns the user to the gallery
				zoomTo(1.0f);
				return true;
			}
		}
		return super.onKeyUp(keyCode, event);
	}

	@Override
	public void setImageBitmap(Bitmap bitmap) {
		setImageBitmap(bitmap, 0);
	}

	private void setImageBitmap(Bitmap bitmap, int rotation) {
		super.setImageBitmap(bitmap);
		Drawable d = getDrawable();
		if (d != null) {
			d.setDither(true);
		}

		Bitmap old = bitmapDisplayed.getBitmap();
		bitmapDisplayed.setBitmap(bitmap);
		bitmapDisplayed.setRotation(rotation);

		if (old != null && old != bitmap && recycler != null) {
			recycler.recycle(old);
		}
	}

	public void clear() {
		setImageBitmapResetBase(null, true);
	}

	// This function changes bitmap, reset base matrix according to the size
	// of the bitmap, and optionally reset the supplementary matrix
	public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
		setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp);
	}

	public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) {
		final int viewWidth = getWidth();

		if (viewWidth <= 0) {
			onLayoutRunnable = new Runnable() {
				public void run() {
					setImageRotateBitmapResetBase(bitmap, resetSupp);
				}
			};
			return;
		}

		if (bitmap.getBitmap() != null) {
			getProperBaseMatrix(bitmap, baseMatrix, true);
			setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
		} else {
			baseMatrix.reset();
			setImageBitmap(null);
		}

		if (resetSupp) {
			suppMatrix.reset();
		}
		setImageMatrix(getImageViewMatrix());
		maxZoom = calculateMaxZoom();
	}

	// Center as much as possible in one or both axis. Centering is defined as
	// follows:
	// * If the image is scaled down below the view's dimensions then center it.
	// * If the image is scaled larger than the view and is translated out of
	// view then translate it back into view.
	protected void center() {
		final Bitmap bitmap = bitmapDisplayed.getBitmap();
		if (bitmap == null) {
			return;
		}
		Matrix m = getImageViewMatrix();

		RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
		m.mapRect(rect);

		float height = rect.height();
		float width = rect.width();

		float deltaX = 0, deltaY = 0;

		deltaY = centerVertical(rect, height, deltaY);
		deltaX = centerHorizontal(rect, width, deltaX);

		postTranslate(deltaX, deltaY);
		setImageMatrix(getImageViewMatrix());
	}

	private float centerVertical(RectF rect, float height, float deltaY) {
		int viewHeight = getHeight();
		if (height < viewHeight) {
			deltaY = (viewHeight - height) / 2 - rect.top;
		} else if (rect.top > 0) {
			deltaY = -rect.top;
		} else if (rect.bottom < viewHeight) {
			deltaY = getHeight() - rect.bottom;
		}
		return deltaY;
	}

	private float centerHorizontal(RectF rect, float width, float deltaX) {
		int viewWidth = getWidth();
		if (width < viewWidth) {
			deltaX = (viewWidth - width) / 2 - rect.left;
		} else if (rect.left > 0) {
			deltaX = -rect.left;
		} else if (rect.right < viewWidth) {
			deltaX = viewWidth - rect.right;
		}
		return deltaX;
	}

	private void init() {
		setScaleType(ScaleType.MATRIX);
	}

	protected float getValue(Matrix matrix, int whichValue) {
		matrix.getValues(matrixValues);
		return matrixValues[whichValue];
	}

	// Get the scale factor out of the matrix.
	protected float getScale(Matrix matrix) {
		return getValue(matrix, Matrix.MSCALE_X);
	}

	protected float getScale() {
		return getScale(suppMatrix);
	}

	// Setup the base matrix so that the image is centered and scaled properly.
	private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) {
		float viewWidth = getWidth();
		float viewHeight = getHeight();

		float w = bitmap.getWidth();
		float h = bitmap.getHeight();
		matrix.reset();

		// We limit up-scaling to 3x otherwise the result may look bad if it's a
		// small icon
		float widthScale = Math.min(viewWidth / w, 3.0f);
		float heightScale = Math.min(viewHeight / h, 3.0f);
		float scale = Math.min(widthScale, heightScale);

		if (includeRotation) {
			matrix.postConcat(bitmap.getRotateMatrix());
		}
		matrix.postScale(scale, scale);
		matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
	}

	// Combine the base matrix and the supp matrix to make the final matrix
	protected Matrix getImageViewMatrix() {
		// The final matrix is computed as the concatentation of the base matrix
		// and the supplementary matrix
		displayMatrix.set(baseMatrix);
		displayMatrix.postConcat(suppMatrix);
		return displayMatrix;
	}

	public Matrix getUnrotatedMatrix() {
		Matrix unrotated = new Matrix();
		getProperBaseMatrix(bitmapDisplayed, unrotated, false);
		unrotated.postConcat(suppMatrix);
		return unrotated;
	}

	protected float calculateMaxZoom() {
		if (bitmapDisplayed.getBitmap() == null) {
			return 1F;
		}

		float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth;
		float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight;
		return Math.max(fw, fh) * 4; // 400%
	}

	protected void zoomTo(float scale, float centerX, float centerY) {
		if (scale > maxZoom) {
			scale = maxZoom;
		}

		float oldScale = getScale();
		float deltaScale = scale / oldScale;

		suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
		setImageMatrix(getImageViewMatrix());
		center();
	}

	protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) {
		final float incrementPerMs = (scale - getScale()) / durationMs;
		final float oldScale = getScale();
		final long startTime = System.currentTimeMillis();

		handler.post(new Runnable() {
			public void run() {
				long now = System.currentTimeMillis();
				float currentMs = Math.min(durationMs, now - startTime);
				float target = oldScale + (incrementPerMs * currentMs);
				zoomTo(target, centerX, centerY);

				if (currentMs < durationMs) {
					handler.post(this);
				}
			}
		});
	}

	protected void zoomTo(float scale) {
		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;
		zoomTo(scale, cx, cy);
	}

	protected void zoomIn() {
		zoomIn(SCALE_RATE);
	}

	protected void zoomOut() {
		zoomOut(SCALE_RATE);
	}

	protected void zoomIn(float rate) {
		if (getScale() >= maxZoom) {
			return; // Don't let the user zoom into the molecular level
		}
		if (bitmapDisplayed.getBitmap() == null) {
			return;
		}

		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;

		suppMatrix.postScale(rate, rate, cx, cy);
		setImageMatrix(getImageViewMatrix());
	}

	protected void zoomOut(float rate) {
		if (bitmapDisplayed.getBitmap() == null) {
			return;
		}

		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;

		// Zoom out to at most 1x
		Matrix tmp = new Matrix(suppMatrix);
		tmp.postScale(1F / rate, 1F / rate, cx, cy);

		if (getScale(tmp) < 1F) {
			suppMatrix.setScale(1F, 1F, cx, cy);
		} else {
			suppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
		}
		setImageMatrix(getImageViewMatrix());
		center();
	}

	protected void postTranslate(float dx, float dy) {
		suppMatrix.postTranslate(dx, dy);
	}

	protected void panBy(float dx, float dy) {
		postTranslate(dx, dy);
		setImageMatrix(getImageViewMatrix());
	}

}
